using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.CSharp.RuntimeHelpers;
using Roslyn.Compilers;
using Roslyn.Compilers.Common;
using Roslyn.Utilities;
using Ref = System.Reflection;
using RefEmit = System.Reflection.Emit;
using System.Globalization;

namespace Roslyn.Scripting
{
    /// <summary>
    /// Represents a runtime execution context for C# scripts.
    /// </summary>
    public abstract class CommonScriptEngine
    {
        public static readonly ReadOnlyArray<string> DefaultReferenceSearchPaths;
        
        protected readonly MetadataFileProvider metadataFileProvider;

        /// <summary>
        /// Unique prefix for generated uncollectible assemblies.
        /// </summary>
        /// <remarks>
        /// The full names of uncollectible assemblies generated by this context must be unique,
        /// so that we can resolve references among them. Note that CLR can load two different assemblies of the very 
        /// identity into the same load context.
        /// 
        /// We are using a certain naming scheme for the generated assemblies (a fixed name prefix followed by a number). 
        /// If we allowed the compiled code to add references that match this exact pattern it migth happen that 
        /// the user supplied reference identity conflicts with the identity we use for our generated assemblies and 
        /// the AppDomain assembly resolve event won't be able to correctly identify the target assembly.
        /// 
        /// To avoid this problem we use a prefix for assemblies we generate that is unlikely to conflict with user specified references.
        /// We also check that no user provided references are allowed to be used in the compiled code and report an error ("reserved assembly name").
        /// </remarks>
        private static readonly string globalAssemblyNamePrefix;
        private static int engineIdDispenser;
        private int submissionIdDispenser = -1;
        private readonly string assemblyNamePrefix;

        // dynamic code storage
        //
        // TODO (tomat): Dynamic assembly allocation strategy. A dynamic assembly is a unit of
        // collection. We need to find a balance between creating a new assembly per execution
        // (potentially shorter code lifetime) and reusing a single assembly for all executions
        // (less overhead per execution).
        
        private readonly UncollectibleCodeManager uncollectibleCodeManager;
        private readonly CollectibleCodeManager collectibleCodeManager;

        // state captured by session at creation time:
        private List<MetadataReference> references;
        private List<string> importedNamespaces;
        private FileResolver fileResolver;

        #region Testing and Debugging

        private const string CollectibleModuleFileName = "CollectibleModule.dll";
        private const string UncollectibleModuleFileName = "UncollectibleModule.dll";

        // Setting this flag will add DebuggableAttribute on the emitted code that disables JIT optimizations.
        // With optimizations disabled JIT will verify the method before it compiles it so we can easily 
        // discover incorrect code.
        internal static bool DisableJitOptimizations;

#if DEBUG
        // Flags to make debugging of emitted code easier.

        // Enables saving the dynamic assemblies to disk. Must be called before any code is compiled. 
        internal static bool EnableAssemblySave;

        // Helps debugging issues in emitted code. If set the next call to Execute/Compile will save the dynamic assemblies to disk.
        internal static bool SaveCompiledAssemblies;
#endif

        #endregion

        static CommonScriptEngine()
        {
            DefaultReferenceSearchPaths = ReadOnlyArray<string>.CreateFrom(RuntimeEnvironment.GetRuntimeDirectory());
            globalAssemblyNamePrefix = "\u211B*" + Guid.NewGuid().ToString() + "-";
        }

        internal CommonScriptEngine(MetadataFileProvider metadataFileProvider, IAssemblyLoader assemblyLoader)
        {
            if (metadataFileProvider == null)
            {
                metadataFileProvider = MetadataFileProvider.Default;
            }

            if (assemblyLoader == null)
            {
                assemblyLoader = new AssemblyLoader();
            }

            var initialReferences = new List<MetadataReference>()
            {
                metadataFileProvider.GetReference(typeof(object).Assembly.Location, MetadataReferenceProperties.Assembly),
                metadataFileProvider.GetReference(typeof(Session).Assembly.Location, MetadataReferenceProperties.Assembly),
            };

            this.assemblyNamePrefix = globalAssemblyNamePrefix + "#" + Interlocked.Increment(ref engineIdDispenser);
            this.metadataFileProvider = metadataFileProvider;
            this.collectibleCodeManager = new CollectibleCodeManager(assemblyLoader, assemblyNamePrefix);
            this.uncollectibleCodeManager = new UncollectibleCodeManager(assemblyLoader, assemblyNamePrefix);

            string initialBaseDirectory;
            try 
            {
                initialBaseDirectory = Directory.GetCurrentDirectory();
            }
            catch
            {
                initialBaseDirectory = null;
            }

            this.fileResolver = CreateFileResolver(DefaultReferenceSearchPaths, initialBaseDirectory);
            this.importedNamespaces = new List<string>();
            this.references = initialReferences;
        }

        public MetadataFileProvider MetadataFileProvider
        {
            get { return metadataFileProvider; }
        }

        public IAssemblyLoader AssemblyLoader 
        { 
            get { return collectibleCodeManager.assemblyLoader; } 
        }

        // TODO (tomat): Consider exposing FileResolver and removing BaseDirectory.
        // We would need WithAssemblySearchPaths on FileResolver to implement SetReferenceSearchPaths 
        internal FileResolver FileResolver
        {
            get 
            { 
                return fileResolver; 
            }

            // for testing
            set
            {
                Debug.Assert(value != null);
                fileResolver = value;
            }
        }

        internal string AssemblyNamePrefix
        {
            get { return assemblyNamePrefix; }
        }

        internal static bool IsReservedAssemblyName(AssemblyIdentity identity)
        {
            return identity.Name.StartsWith(globalAssemblyNamePrefix);
        }

        internal int GenerateSubmissionId(out string assemblyName, out string typeName)
        {
            int id = Interlocked.Increment(ref submissionIdDispenser);
            assemblyName = assemblyNamePrefix + id;
            typeName = "Submission#" + id;
            return id;
        }

        #region Session

        // for testing only:
        // TODO (tomat): Sessions generate uncollectible code since we don't know whether they would execute just one submissions or multiple.
        // We need to address code collectibility of multi-submission sessions at some point (the CLR needs to be fixed). Meanwhile use this helper 
        // to force collectible code generation in order to keep test coverage.
        internal Session CreateCollectibleSession()
        {
            return new Session(this, hostObject: null, hostObjectType: null, isCollectible: true);
        }

        internal Session CreateCollectibleSession<THostObject>(THostObject hostObject)
        {
            return new Session(this, hostObject, typeof(THostObject), isCollectible: true);
        }

        public Session CreateSession()  // TODO (tomat): bool isCancellable = false
        {
            return new Session(this, hostObject: null, hostObjectType: null, isCollectible: false);
        }

        public Session CreateSession(object hostObject) // TODO (tomat): bool isCancellable = false
        {
            if (hostObject == null)
            {
                throw new ArgumentNullException("hostObject");
            }

            return new Session(this, hostObject, hostObjectType: hostObject.GetType(), isCollectible: false);
        }

        public Session CreateSession(object hostObject, Type hostObjectType) // TODO (tomat): bool isCancellable = false
        {
            if (hostObject == null) 
            {
                throw new ArgumentNullException("hostObject");
            }

            if (hostObjectType == null) 
            {
                throw new ArgumentNullException("hostObjectType");
            }

            Type actualType = hostObject.GetType();
            if (!hostObjectType.IsAssignableFrom(actualType))
            {
                throw new ArgumentException(String.Format("Can't assign '{0}' to '{1}'.".NeedsLocalization(), actualType, hostObjectType), "hostObjectType");
            }

            return new Session(this, hostObject, hostObjectType, isCollectible: false);
        }

        public Session CreateSession<THostObject>(THostObject hostObject) // TODO (tomat): bool isCancellable = false
            where THostObject : class
        {
            if (hostObject == null)
            {
                throw new ArgumentNullException("hostObject");
            }

            return new Session(this, hostObject, typeof(THostObject), isCollectible: false);
        }

        #endregion

        #region State

        /// <summary>
        /// The base directory used to resolve relative paths to assembly references and 
        /// relative paths that appear in source code compiled by this script engine.
        /// </summary>
        /// <remarks>
        /// If null relative paths won't be resolved and an error will be reported when the compiler encountrs such paths.
        /// The value can be changed at any point in time. However the new value doesn't affect already compiled submissions.
        /// The initial value is the current working directory if the current process, or null if not available.
        /// Changing the base directory doesn't affect the process current working directory used by <see cref="System.IO"/> APIs.
        /// </remarks>
        public string BaseDirectory
        {
            get 
            { 
                return fileResolver.BaseDirectory; 
            }
            set 
            {
                if (fileResolver.BaseDirectory != value)
                {
                    fileResolver = CreateFileResolver(ReferenceSearchPaths, value);
                }
            }
        }

        public ReadOnlyArray<string> ReferenceSearchPaths
        {
            get { return fileResolver.AssemblySearchPaths; }
        }

        public void SetReferenceSearchPaths(params string[] paths)
        {
            SetReferenceSearchPaths((IEnumerable<string>)paths);
        }

        public void SetReferenceSearchPaths(IEnumerable<string> paths)
        {
            SetReferenceSearchPaths(ReadOnlyArray<string>.CreateFrom(paths));
        }

        public void SetReferenceSearchPaths(ReadOnlyArray<string> paths)
        {
            if (paths != fileResolver.AssemblySearchPaths)
            {
                FileResolver.ValidateSearchPaths(paths, "paths");
                fileResolver = CreateFileResolver(paths, BaseDirectory);
            }
        }

        /// <summary>
        /// Returns a list of assemblies that are currently referenced by the engine.
        /// </summary>
        public ReadOnlyArray<MetadataReference> GetReferences()
        {
            return ReadOnlyArray<MetadataReference>.CreateFrom(references);
        }

        /// <summary>
        /// Adds a reference to specified assembly.
        /// </summary>
        /// <param name="assemblyDisplayNameOrPath">Assembly display name or path.</param>
        /// <exception cref="ArgumentNullException"><paramref name="assemblyDisplayNameOrPath"/> is null.</exception>
        /// <exception cref="ArgumentException"><paramref name="assemblyDisplayNameOrPath"/> is empty.</exception>
        /// <exception cref="FileNotFoundException">Assembly file can't be found.</exception>
        public void AddReference(string assemblyDisplayNameOrPath)
        {
            AddReference(CreateMetadataReference(assemblyDisplayNameOrPath, this.fileResolver));
        }

        /// <summary>
        /// Adds a reference to specified assembly.
        /// </summary>
        /// <param name="assembly">Runtime assembly. The assembly must be loaded from a file on disk. In-memory assemblies are not supported.</param>
        /// <exception cref="ArgumentNullException"><paramref name="assembly"/> is null.</exception>
        public void AddReference(Ref.Assembly assembly)
        {
            AddReference(CreateMetadataReference(assembly));
        }

        /// <summary>
        /// Adds a reference to specified assembly.
        /// </summary>
        /// <param name="reference">Assembly reference.</param>
        /// <exception cref="ArgumentException"><paramref name="reference"/> is not an assembly reference (it's a module).</exception>
        /// <exception cref="ArgumentNullException"><paramref name="reference"/> is null.</exception>
        public void AddReference(MetadataReference reference)
        {
            if (reference == null)
            {
                throw new ArgumentNullException("reference");
            }

            if (reference.Properties.Kind != MetadataImageKind.Assembly)
            {
                throw new ArgumentException("Expected an assembly reference.".NeedsLocalization(), "reference");
            }

            references.Add(reference);
        }

        internal MetadataReference CreateMetadataReference(Ref.Assembly assembly)
        {
            if (assembly == null)
            {
                throw new ArgumentNullException("assembly");
            }

            // assembly location should be a normalized path
            Debug.Assert(string.Equals(FileUtilities.NormalizeAbsolutePath(assembly.Location), assembly.Location, StringComparison.OrdinalIgnoreCase));

            return metadataFileProvider.GetReference(assembly.Location);
        }

        internal MetadataReference CreateMetadataReference(string assemblyDisplayNameOrPath, FileResolver fileResolver)
        {
            if (assemblyDisplayNameOrPath == null)
            {
                throw new ArgumentNullException("assemblyDisplayNameOrPath");
            }

            if (assemblyDisplayNameOrPath.Length == 0)
            {
                throw new ArgumentException("Display name or path cannot be empty.".NeedsLocalization(), "assemblyDisplayNameOrPath");
            }

            string fullPath = fileResolver.ResolveMetadataReference(assemblyDisplayNameOrPath);
            if (fullPath == null)
            {
                throw new FileNotFoundException("Assembly not found.".NeedsLocalization(), assemblyDisplayNameOrPath);
            }

            return metadataFileProvider.GetReference(fullPath);
        }

        internal static FileResolver CreateFileResolver(ReadOnlyArray<string> referenceSearchPaths, string baseDirectory)
        {
            return new FileResolver(
                referenceSearchPaths,
                keyFileSearchPaths: ReadOnlyArray<string>.Empty,
                baseDirectory: baseDirectory,
                architectureFilter: GlobalAssemblyCache.CurrentArchitectureFilter,
                preferredCulture: CultureInfo.CurrentCulture); // TODO (tomat): add ReferenceCulture property to ScriptEngine?
        }

        /// <summary>
        /// Returns a list of imported namespaces.
        /// </summary>
        public ReadOnlyArray<string> GetImportedNamespaces()
        {
            return ReadOnlyArray<string>.CreateFrom(importedNamespaces);
        }

        /// <summary>
        /// Imports a namespace, an equivalent of executing "using <paramref name="namespace"/>;" (C#) or "Imports <paramref name="namespace"/>" (VB).
        /// </summary>
        /// <exception cref="ArgumentNullException"><paramref name="namespace"/> is null.</exception>
        /// <exception cref="ArgumentException"><paramref name="namespace"/> is not a valid namespace name.</exception>
        public void ImportNamespace(string @namespace)
        {
            ValidateNamespace(@namespace);

            // we don't report duplicates to get the same behavior as evaluating "using NS;" twice.
            importedNamespaces.Add(@namespace);
        }

        internal static void ValidateNamespace(string @namespace)
        {
            if (@namespace == null)
            {
                throw new ArgumentNullException("namespace");
            } 
            
            // Only check that the namespace is a CLR namespace name.
            // If the namespace doesn't exist an error will be reported when compiling the next submission.

            if (!@namespace.IsValidClrNamespaceName())
            {
                throw new ArgumentException("Invalid namespace name", "namespace");
            }
        }

        #endregion

        #region Code Compilation And Execution

        internal T Execute<T>(string code, string path, DiagnosticBag diagnostics, Session session, bool isInteractive)
        {
            Debug.Assert(session != null);

            CommonCompilation compilation;
            Delegate factory;
            if (Compile(code, path, diagnostics, session, typeof(Func<Session, T>), typeof(T), default(CancellationToken), 
                isInteractive: true, isExecute: true, compilation: out compilation, factory: out factory))
            {
                return (factory != null) ? ((Func<Session, T>)factory)(session) : default(T);
            }

            Debug.Assert(diagnostics != null);
            return default(T);
        }

        internal Submission<T> CompileSubmission<T>(string code, Session session, string path = null, bool isInteractive = true)
        {
            CommonCompilation compilation;
            Delegate factory;
            return Compile(
                code, 
                path ?? "",
                diagnostics: null, 
                session: session, 
                delegateType: typeof(Func<Session, T>), 
                returnType: typeof(T), 
                cancellationToken: default(CancellationToken), 
                isInteractive: isInteractive,
                isExecute: false,
                compilation: out compilation, 
                factory: out factory) 
                ? new Submission<T>(compilation, session, (Func<Session, T>)factory) : null;
        }

        private bool Compile(
            string code,
            string path,
            DiagnosticBag diagnostics,
            Session session,
            Type delegateType,
            Type returnType,
            CancellationToken cancellationToken,
            bool isInteractive,
            bool isExecute,
            out CommonCompilation compilation,
            out Delegate factory)
        {
            Debug.Assert(delegateType != null);
            Debug.Assert(session != null);
            Debug.Assert(code != null);

            DiagnosticBag localDiagnostics = diagnostics ?? DiagnosticBag.GetInstance();
            IText text = new StringText(code);
            try
            {
                CommonCompilation localCompilation = CreateCompilation(text, path, isInteractive, session, returnType, localDiagnostics);
                if (localCompilation == null)
                {
                    Debug.Assert(localDiagnostics.HasAnyErrors());
                    CompilationError(localDiagnostics, diagnostics);
                    factory = null;
                    compilation = null;
                    return false;
                }

                // throw away all syntax warnings, they will be reported again by emit:
                localDiagnostics.Clear();

                // If we compile just in time for execution the user doesn't have access to the submission compilation just created.
                // If the previous submission hasn't been executed yet the execution would blow up when invoking the submission factory,
                // and after that the session would become unusable since there is no way how the user can re-execute the current submission.
                // So throw here before we save the submission into the session.
                if (isExecute)
                {
                    int slotIndex = localCompilation.GetSubmissionSlotIndex();
                    SessionHelpers.RequirePreviousSubmissionExecuted(session, slotIndex);
                }

                // TODO: (tomat)
                // Current CLR implementation of collectible assemblies doesn't support 
                // references from uncollectible to collectible code.
                //
                // We can't emit collectible code if there is a possibility that we will
                // compile another submission in the session.
                // 
                // A workaround might be to regenerate all collectible dependencies of an uncollectible 
                // submission as uncollectible.
                // 
                // For now we explicitly mark sessions as collectible in tests to keep test coverage.
                bool collectible = session.isCollectible;
                if (!TryEmitSubmission(localCompilation, localDiagnostics, delegateType, collectible, cancellationToken, out factory))
                {
                    Debug.Assert(localDiagnostics.HasAnyErrors());
                    CompilationError(localDiagnostics, diagnostics);
                    factory = null;
                    compilation = null;
                    return false;
                }

                session.SubmissionCompiled(localCompilation);

                compilation = localCompilation;
                return true;
            }
            finally
            {
                if (localDiagnostics != diagnostics)
                {
                    localDiagnostics.Free();
                }
            }
        }

        internal abstract CommonCompilation CreateCompilation(IText text, string path, bool isInteractive, Session session, Type returnType, DiagnosticBag localDiagnostics);
        
#if TODO
        public Func<TContext, TContract> Compile<TContext, TContract>(string sourceCode)
        {
            throw new NotImplementedException();
        }

        public Func<TContext, dynamic> Compile<TContext>(string sourceCode)
        {
            throw new NotImplementedException();
        }

        public Func<dynamic> Compile(string sourceCode)
        {
            throw new NotImplementedException();
        }

        public T CompileToMethod<T>(string sourceCode, string[] parameterNames = null)
        {
            throw new NotImplementedException();
        }
#endif

        internal static void CompilationError(DiagnosticBag localDiagnostics, DiagnosticBag diagnostics)
        {
            if (diagnostics != null)
            {
                return;
            }

            var firstError = localDiagnostics.FirstOrDefault(d => d.Info.Severity == DiagnosticSeverity.Error);
            if (firstError != null)
            {
                throw new CompilationErrorException(firstError.ToString(System.Globalization.CultureInfo.CurrentCulture),
                    new ReadOnlyArray<CommonDiagnostic>(localDiagnostics.ToArray()));
            }
        }

        private bool TryEmitSubmission(CommonCompilation compilation, DiagnosticBag diagnostics, Type delegateType, bool collectible,
            CancellationToken cancellationToken, out Delegate factory)
        {
            // Note that we migth be giving the emitter a collectible module but if the code contains uncollectible features
            // it will be generated to a separate new uncollectible assembly.
            var emitResult = compilation.Emit(
                GetOrCreateDynamicModule(collectible),
                assemblyLoader: GetAssemblyLoader(collectible),
                assemblySymbolMapper: symbol => MapAssemblySymbol(symbol, collectible),
                cancellationToken: cancellationToken
            );

            diagnostics.Add(emitResult.Diagnostics);

            // emit can fail due to compilation errors or because there is nothing to emit:
            if (!emitResult.Success)
            {
                factory = null;
                return !diagnostics.HasAnyErrors();
            }

            Debug.Assert(emitResult.EntryPoint != null);

            if (emitResult.IsUncollectible)
            {
                // Ref.Emit wasn't able to emit the assembly
                uncollectibleCodeManager.AddFallBackAssembly(emitResult.EntryPoint.DeclaringType.Assembly);
            }
#if DEBUG
            if (SaveCompiledAssemblies)
            {
                this.uncollectibleCodeManager.Save(UncollectibleModuleFileName);
                this.collectibleCodeManager.Save(CollectibleModuleFileName);
            }
#endif
            factory = Delegate.CreateDelegate(delegateType, emitResult.EntryPoint);
            return true;
        }

        #endregion

        #region Assembly Resolution
        
        internal RefEmit.ModuleBuilder GetOrCreateDynamicModule(bool collectible)
        {
            if (collectible)
            {
                return collectibleCodeManager.GetOrCreateDynamicModule();
            }
            else
            {
                return uncollectibleCodeManager.GetOrCreateDynamicModule();
            }
        }

        private static RefEmit.ModuleBuilder CreateDynamicModule(RefEmit.AssemblyBuilderAccess access, AssemblyIdentity name, string fileName)
        {
            var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(name.ToAssemblyName(), access);

            if (DisableJitOptimizations) 
            {
                assemblyBuilder.SetCustomAttribute(new RefEmit.CustomAttributeBuilder(
                    typeof(DebuggableAttribute).GetConstructor(new[] { typeof(DebuggableAttribute.DebuggingModes) }),
                    new object[] {  DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations }));
            }

            const string moduleName = "InteractiveModule";

            if (access == RefEmit.AssemblyBuilderAccess.RunAndSave)
            {
                return assemblyBuilder.DefineDynamicModule(moduleName, fileName, emitSymbolInfo: false);
            }
            else
            {
                return assemblyBuilder.DefineDynamicModule(moduleName, emitSymbolInfo: false);
            }
        }

        /// <summary>
        /// Maps given assembly symbol to an assembly ref.
        /// </summary>
        /// <remarks>
        /// The compiler represents every submission by a compilation instance for which it creates a distinct source assembly symbol.
        /// However multiple submissions might compile into a single dynamic assembly and so we need to map the corresponding assembly symbols to 
        /// the name of the dynamic assembly.
        /// </remarks>
        internal AssemblyIdentity MapAssemblySymbol(IAssemblySymbol symbol, bool collectible)
        {
            if (symbol.IsInteractive)
            {
                if (collectible)
                {
                    // collectible assemblies can't reference other generated assemblies
                    throw Contract.Unreachable;
                }
                else if (!uncollectibleCodeManager.ContainsAssembly(symbol.Identity.Name))
                {
                    // uncollectible assemblies can reference uncollectible dynamic or uncollectible CCI generated assemblies:
                    return uncollectibleCodeManager.dynamicAssemblyName;
                }
            }

            return symbol.Identity;
        }

        internal IAssemblyLoader GetAssemblyLoader(bool collectible)
        {
            return collectible ? (IAssemblyLoader)collectibleCodeManager : uncollectibleCodeManager;
        }

        // TODO (tomat): the code managers can be improved - common base class, less locking, etc.

        private sealed class CollectibleCodeManager : IAssemblyLoader
        {
            internal readonly IAssemblyLoader assemblyLoader;
            private readonly AssemblyIdentity dynamicAssemblyName;

            // lock(this) on access
            internal RefEmit.ModuleBuilder dynamicModule;

            public CollectibleCodeManager(IAssemblyLoader assemblyLoader, string assemblyNamePrefix)
            {
                this.assemblyLoader = assemblyLoader;
                this.dynamicAssemblyName = new AssemblyIdentity(name: assemblyNamePrefix + "CD");

                AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(Resolve);
            }

            internal RefEmit.ModuleBuilder GetOrCreateDynamicModule()
            {
                if (dynamicModule == null)
                {
                    DebuggerUtilities.CallBeforeAcquiringLock(); //see method comment

                    lock (this)
                    {
                        if (dynamicModule == null)
                        {
                            dynamicModule = CreateDynamicModule(
#if DEBUG
                                EnableAssemblySave ? RefEmit.AssemblyBuilderAccess.RunAndSave :
#endif
                                RefEmit.AssemblyBuilderAccess.RunAndCollect, dynamicAssemblyName, CollectibleModuleFileName);
                        }
                    }
                }

                return dynamicModule;
            }

            internal void Save(string fileName)
            {
                if (dynamicModule != null)
                {
                    ((RefEmit.AssemblyBuilder)dynamicModule.Assembly).Save(fileName);
                }
            }

            private Ref.Assembly Resolve(object sender, ResolveEventArgs args)
            {
                if (args.Name != dynamicAssemblyName.GetDisplayName())
                {
                    return null;
                }
                    
                lock (this)
                {
                    return (dynamicModule != null) ? dynamicModule.Assembly : null;
                }
            }

            Ref.Assembly IAssemblyLoader.Load(AssemblyIdentity identity)
            {
                if (dynamicModule != null && identity.Name == dynamicAssemblyName.Name)
                {
                    return dynamicModule.Assembly;
                }

                return assemblyLoader.Load(identity);
            }
        }

        /// <summary>
        /// Manages uncollectible assemblies and resolves assembly references baked into CCI generated metadata. 
        /// The resolution is triggered by the CLR Type Loader.
        /// </summary>
        private sealed class UncollectibleCodeManager : IAssemblyLoader
        {
            private readonly IAssemblyLoader assemblyLoader;
            private readonly string assemblyNamePrefix;
            internal readonly AssemblyIdentity dynamicAssemblyName;

            // lock(this) on access
            private RefEmit.ModuleBuilder dynamicModule;      // primary uncollectible assembly
            private HashSet<Ref.Assembly> fallBackAssemblies; // additional uncollectible assemblies created due to a Ref.Emit falling back to CCI
            private Dictionary<string, Ref.Assembly> mapping; // { simple name -> fall-back assembly }

            internal UncollectibleCodeManager(IAssemblyLoader assemblyLoader, string assemblyNamePrefix)
            {
                this.assemblyLoader = assemblyLoader;
                this.assemblyNamePrefix = assemblyNamePrefix;
                this.dynamicAssemblyName = new AssemblyIdentity(name: assemblyNamePrefix + "UD");

                AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(Resolve);
            }

            internal RefEmit.ModuleBuilder GetOrCreateDynamicModule()
            {
                if (dynamicModule == null)
                {
                    DebuggerUtilities.CallBeforeAcquiringLock(); //see method comment

                    lock (this)
                    {
                        if (dynamicModule == null)
                        {
                            dynamicModule = CreateDynamicModule(
#if DEBUG
                                EnableAssemblySave ? RefEmit.AssemblyBuilderAccess.RunAndSave :
#endif
                                RefEmit.AssemblyBuilderAccess.Run, dynamicAssemblyName, UncollectibleModuleFileName);
                        }
                    }
                }

                return dynamicModule;
            }

            internal void Save(string fileName)
            {
                if (dynamicModule != null)
                {
                    ((RefEmit.AssemblyBuilder)dynamicModule.Assembly).Save(fileName);
                }
            }

            internal void AddFallBackAssembly(Ref.Assembly assembly)
            {
                DebuggerUtilities.CallBeforeAcquiringLock(); //see method comment

                lock (this)
                {
                    if (fallBackAssemblies == null)
                    {
                        Debug.Assert(mapping == null);
                        fallBackAssemblies = new HashSet<Ref.Assembly>();
                        mapping = new Dictionary<string, Ref.Assembly>();
                    }

                    fallBackAssemblies.Add(assembly);
                    mapping[assembly.GetName().Name] = assembly;
                }
            }

            internal bool ContainsAssembly(string simpleName)
            {
                if (mapping == null)
                {
                    return false;
                }

                DebuggerUtilities.CallBeforeAcquiringLock(); //see method comment

                lock (this)
                {
                    return mapping.ContainsKey(simpleName);
                }
            }

            private Ref.Assembly Resolve(object sender, ResolveEventArgs args)
            {
                if (!args.Name.StartsWith(assemblyNamePrefix))
                {
                    return null;
                }

                DebuggerUtilities.CallBeforeAcquiringLock(); //see method comment

                lock (this)
                {
                    if (args.Name == dynamicAssemblyName.GetDisplayName())
                    {
                        return dynamicModule != null ? dynamicModule.Assembly : null;
                    }

                    if (dynamicModule != null && dynamicModule.Assembly == args.RequestingAssembly ||
                        fallBackAssemblies != null && fallBackAssemblies.Contains(args.RequestingAssembly))
                    {
                        int comma = args.Name.IndexOf(',');
                        return ResolveNoLock(args.Name.Substring(0, (comma != -1) ? comma : args.Name.Length));
                    }
                }

                return null;
            }

            private Ref.Assembly Resolve(string simpleName)
            {
                DebuggerUtilities.CallBeforeAcquiringLock(); //see method comment

                lock (this)
                {
                    return ResolveNoLock(simpleName);
                }
            }

            private Ref.Assembly ResolveNoLock(string simpleName)
            {
                if (dynamicModule != null && simpleName == dynamicAssemblyName.Name)
                {
                    return dynamicModule.Assembly;
                }

                Ref.Assembly assembly;
                if (mapping != null && mapping.TryGetValue(simpleName, out assembly))
                {
                    return assembly;
                }

                return null;
            }

            Ref.Assembly IAssemblyLoader.Load(AssemblyIdentity identity)
            {
                return Resolve(identity.Name) ?? assemblyLoader.Load(identity);
            }
        }

        #endregion
    }
}
